{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9a47c35e-5fd7-48ca-b865-d07d76d3cfff",
   "metadata": {},
   "source": [
    "# L1: Vanilla Vector Search\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01acdf11-0273-4a1a-9b35-090bfeb44343",
   "metadata": {},
   "source": [
    "<p style=\"background-color:#fff6e4; padding:15px; border-width:3px; border-color:#f5ecda; border-style:solid; border-radius:6px\"> ⏳ <b>Note <code>(Kernel Starting)</code>:</b> This notebook takes about 30 seconds to be ready to use. You may start and watch the video while you wait.</p>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d8062eca-e2de-4c79-ad94-9b4ad8e70d7e",
   "metadata": {
    "height": 64
   },
   "outputs": [],
   "source": [
    "# Warning control\n",
    "import warnings\n",
    "warnings.filterwarnings('ignore')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b5cd7ff-ff61-4fc2-a246-b6ffeaa2869e",
   "metadata": {
    "height": 47
   },
   "outputs": [],
   "source": [
    "#!pip install datasets pandas openai pymongo pydantic"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "835995e2-98e6-46cb-8050-9dce6d44f5c8",
   "metadata": {},
   "source": [
    "## Get API Keys\n",
    "In this classroom, the libraries and APIs have been already installed and set up for you.\n",
    "If you would like to run this code on your own machine, you will need to enter your own MONGO_URI and OPENAI_API_KEY keys in the following cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "32321449-c1ca-4851-95cd-8992418bdc1c",
   "metadata": {
    "height": 115
   },
   "outputs": [],
   "source": [
    "import os\n",
    "from dotenv import load_dotenv, find_dotenv\n",
    "_ = load_dotenv(find_dotenv()) # read local .env file\n",
    "OPENAI_API_KEY = os.environ.get(\"OPENAI_API_KEY\")\n",
    "MONGO_URI = os.environ.get(\"MONGO_URI\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2f7e67f7-f17c-4d18-ba19-ec64d4d544d7",
   "metadata": {},
   "source": [
    "<p style=\"background-color:#fff6ff; padding:15px; border-width:3px; border-color:#efe6ef; border-style:solid; border-radius:6px\"> 💻 &nbsp; <b>Access <code>requirements.txt</code> file:</b> To access <code>requirements.txt</code> for this notebook, 1) click on the <em>\"File\"</em> option on the top menu of the notebook and then 2) click on <em>\"Open\"</em>. For more help, please see the <em>\"Appendix - Tips and Help\"</em> Lesson.</p>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b90f1121-a49e-45fc-af27-6da418c291cc",
   "metadata": {},
   "source": [
    "## 1.1 Data Loading"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6104c624-b988-4ea6-9c79-f76907891507",
   "metadata": {
    "height": 251
   },
   "outputs": [],
   "source": [
    "# 1. Dataset Loading\n",
    "from datasets import load_dataset\n",
    "import pandas as pd\n",
    "\n",
    "# NOTE: Make sure you have an Hugging Face token (HF_TOKEN) in your development environemnt\n",
    "# NOTE: https://huggingface.co/datasets/MongoDB/airbnb_embeddings\n",
    "# NOTE: This dataset contains several records with datapoint representing an airbnb listing.\n",
    "# NOTE: This dataset contains text and image embeddings, but this lessons only uses the text embeddings\n",
    "dataset = load_dataset(\"MongoDB/airbnb_embeddings\", streaming=True, split=\"train\")\n",
    "dataset = dataset.take(100)\n",
    "# Convert the dataset to a pandas dataframe\n",
    "dataset_df = pd.DataFrame(dataset)\n",
    "dataset_df.head(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5194bdd7-f563-4bdb-bc49-b528beaf5543",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": [
    "print(\"Columns:\", dataset_df.columns)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de8bc707-c6bb-4ff4-b801-a3db598ae253",
   "metadata": {},
   "source": [
    "## 1.2 Document Modelling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ab58b04e-510e-44ab-bde0-9ceefdbb80d8",
   "metadata": {
    "height": 64
   },
   "outputs": [],
   "source": [
    "from typing import List, Optional\n",
    "from pydantic import BaseModel, ValidationError\n",
    "from datetime import datetime"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d91abf6f-6272-4577-a6e6-65aa9d06887b",
   "metadata": {
    "height": 234
   },
   "outputs": [],
   "source": [
    "class Host(BaseModel):\n",
    "    host_id: str\n",
    "    host_url: str\n",
    "    host_name: str\n",
    "    host_location: str\n",
    "    host_about: str\n",
    "    host_response_time: Optional[str] = None\n",
    "    host_thumbnail_url: str\n",
    "    host_picture_url: str\n",
    "    host_response_rate: Optional[int] = None\n",
    "    host_is_superhost: bool\n",
    "    host_has_profile_pic: bool\n",
    "    host_identity_verified: bool"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "bb99dd71-377e-4036-8651-36e7292078e1",
   "metadata": {
    "height": 217
   },
   "outputs": [],
   "source": [
    "class Location(BaseModel):\n",
    "    type: str\n",
    "    coordinates: List[float]\n",
    "    is_location_exact: bool\n",
    "\n",
    "class Address(BaseModel):\n",
    "    street: str\n",
    "    government_area: str\n",
    "    market: str\n",
    "    country: str\n",
    "    country_code: str\n",
    "    location: Location"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c647ebab-8e65-4011-8a11-994eefa7051f",
   "metadata": {
    "height": 132
   },
   "outputs": [],
   "source": [
    "class Review(BaseModel):\n",
    "    _id: str\n",
    "    date: Optional[datetime] = None\n",
    "    listing_id: str\n",
    "    reviewer_id: str\n",
    "    reviewer_name: Optional[str] = None\n",
    "    comments: Optional[str] = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a47f72d6-6e88-4de6-b2bc-5c4a60905452",
   "metadata": {
    "height": 744
   },
   "outputs": [],
   "source": [
    "class Listing(BaseModel):\n",
    "    _id: int\n",
    "    listing_url: str\n",
    "    name: str\n",
    "    summary: str\n",
    "    space: str\n",
    "    description: str\n",
    "    neighborhood_overview: Optional[str] = None\n",
    "    notes: Optional[str] = None\n",
    "    transit: Optional[str] = None\n",
    "    access: str\n",
    "    interaction: Optional[str] = None\n",
    "    house_rules: str\n",
    "    property_type: str\n",
    "    room_type: str\n",
    "    bed_type: str\n",
    "    minimum_nights: int\n",
    "    maximum_nights: int\n",
    "    cancellation_policy: str\n",
    "    last_scraped: Optional[datetime] = None\n",
    "    calendar_last_scraped: Optional[datetime] = None\n",
    "    first_review: Optional[datetime] = None\n",
    "    last_review: Optional[datetime] = None\n",
    "    accommodates: int\n",
    "    bedrooms: Optional[float] = 0\n",
    "    beds: Optional[float] = 0\n",
    "    number_of_reviews: int\n",
    "    bathrooms: Optional[float] = 0\n",
    "    amenities: List[str]\n",
    "    price: int\n",
    "    security_deposit: Optional[float] = None\n",
    "    cleaning_fee: Optional[float] = None\n",
    "    extra_people: int\n",
    "    guests_included: int\n",
    "    images: dict\n",
    "    host: Host\n",
    "    address: Address\n",
    "    availability: dict\n",
    "    review_scores: dict\n",
    "    reviews: List[Review]\n",
    "    text_embeddings: List[float]\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3f49d2e-0d2d-4445-84da-1bfd3be8b9ff",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": [
    "records = dataset_df.to_dict(orient='records')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f8e45574-4e91-4773-9da3-2268939dc72a",
   "metadata": {
    "height": 217
   },
   "outputs": [],
   "source": [
    "# To handle catch `NaT` values\n",
    "for record in records:\n",
    "    for key, value in record.items():\n",
    "        # Check if the value is list-like; if so, process each element.\n",
    "        if isinstance(value, list):\n",
    "            processed_list = [None if pd.isnull(v) else v for v in value]\n",
    "            record[key] = processed_list\n",
    "        # For scalar values, continue as before.\n",
    "        else:\n",
    "            if pd.isnull(value):\n",
    "                record[key] = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04783976-9ebd-4153-b0ed-cab12505569d",
   "metadata": {
    "height": 149
   },
   "outputs": [],
   "source": [
    "try:\n",
    "  # Convert each dictionary to a Movie instance\n",
    "  listings = [Listing(**record).dict() for record in records]\n",
    "  # Get an overview of a single datapoint\n",
    "  print(listings[0].keys())\n",
    "except ValidationError as e:\n",
    "  print(e)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8da84aef-d361-4955-b40c-cf3f05600729",
   "metadata": {},
   "source": [
    "## 1.3 Database Creation and Connection"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d0d917b2-f08c-4861-9954-c7941d0b8f0c",
   "metadata": {
    "height": 47
   },
   "outputs": [],
   "source": [
    "from pymongo.mongo_client import MongoClient\n",
    "from pymongo.operations import SearchIndexModel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d076882-9108-4394-8594-f59a12bf2782",
   "metadata": {
    "height": 47
   },
   "outputs": [],
   "source": [
    "database_name = \"airbnb_dataset\"\n",
    "collection_name = \"listings_reviews\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "317d102e-5f93-4444-a7eb-29e0f7954589",
   "metadata": {
    "height": 166
   },
   "outputs": [],
   "source": [
    "\n",
    "def get_mongo_client(mongo_uri):\n",
    "    \"\"\"Establish connection to the MongoDB.\"\"\"\n",
    "\n",
    "    # gateway to interacting with a MongoDB database cluster\n",
    "    client = MongoClient(mongo_uri, appname=\"devrel.deeplearningai.lesson1.python\")\n",
    "    print(\"Connection to MongoDB successful\")\n",
    "    return client"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "abb10772-e0cb-42e3-8b47-71673ec3a130",
   "metadata": {
    "height": 166
   },
   "outputs": [],
   "source": [
    "if not MONGO_URI:\n",
    "    print(\"MONGO_URI not set in environment variables\")\n",
    "\n",
    "mongo_client = get_mongo_client(MONGO_URI)\n",
    "\n",
    "# Pymongo client of database and collection\n",
    "db = mongo_client.get_database(database_name)\n",
    "collection = db.get_collection(collection_name)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6bf7fa64-1abb-4477-9f3d-e7ed5cc6bd34",
   "metadata": {
    "height": 47
   },
   "outputs": [],
   "source": [
    "# Delete any existing records in the collection\n",
    "collection.delete_many({})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ab5db690-01c1-4439-a346-336859563ab2",
   "metadata": {},
   "source": [
    "## 1.4 Data Ingestion"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e4f78e05-9297-4f18-8fd1-8c2198b3e5a6",
   "metadata": {
    "height": 64
   },
   "outputs": [],
   "source": [
    "# The ingestion process might take a few minutes\n",
    "collection.insert_many(listings)\n",
    "print(\"Data ingestion into MongoDB completed\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "504a9de1-86d5-48ea-b5d4-9c52eb0424d8",
   "metadata": {},
   "source": [
    "## 1.5 Vector Search Index defintion"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fb74480e-b469-4d4c-a4e4-a3bdd4acd57a",
   "metadata": {
    "height": 115
   },
   "outputs": [],
   "source": [
    "# NOTE: This dataset contains text and image embeddings, but this lessons only uses the text embeddings\n",
    "# The field containing the text embeddings on each document within the listings_reviews collection \n",
    "text_embedding_field_name = \"text_embeddings\"\n",
    "# MongoDB Atlas Vector Search index name\n",
    "vector_search_index_name_text = \"vector_index_text\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "43a30185-7971-4497-b4b4-059dd0e4a970",
   "metadata": {
    "height": 285
   },
   "outputs": [],
   "source": [
    "vector_search_index_model = SearchIndexModel(\n",
    "    definition={\n",
    "        \"mappings\": { # describes how fields in the database documents are indexed and stored\n",
    "            \"dynamic\": True, # automatically index new fields that appear in the document\n",
    "            \"fields\": { # properties of the fields that will be indexed.\n",
    "                text_embedding_field_name: { \n",
    "                    \"dimensions\": 1536, # size of the vector.\n",
    "                    \"similarity\": \"cosine\", # algorithm used to compute the similarity between vectors\n",
    "                    \"type\": \"knnVector\",\n",
    "                }\n",
    "            },\n",
    "        }\n",
    "    },\n",
    "    name=vector_search_index_name_text, # identifier for the vector search index\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "beeb55e8-2ba2-4205-a0da-549e40662e27",
   "metadata": {
    "height": 149
   },
   "outputs": [],
   "source": [
    "# Check if the index already exists\n",
    "index_exists = False\n",
    "for index in collection.list_indexes():\n",
    "    print(index)\n",
    "    if index['name'] == vector_search_index_name_text:\n",
    "        index_exists = True\n",
    "        break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7dfbcb84-3c10-4ebe-9db1-96c55810a931",
   "metadata": {
    "height": 302
   },
   "outputs": [],
   "source": [
    "import time\n",
    "\n",
    "# Create the index if it doesn't exist\n",
    "if not index_exists:\n",
    "    try:\n",
    "        result = collection.create_search_index(model=vector_search_index_model)\n",
    "        print(\"Creating index...\")\n",
    "        time.sleep(20)  # Sleep for 20 seconds, adding sleep to ensure vector index has compeleted inital sync before utilization\n",
    "        print(\"Index created successfully:\", result)\n",
    "        print(\"Wait a few minutes before conducting search with index to ensure index intialization\")\n",
    "    except Exception as e:\n",
    "        print(f\"Error creating vector search index: {str(e)}\")\n",
    "else:\n",
    "    print(f\"Index '{vector_search_index_name_text}' already exists.\")\n",
    "\n",
    "# NOTE: if the output of this process is Error creating vector search index: Duplicate Index, you may proceed to the next cell if you intend to still use a previously created index"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3d9134f5-9838-491a-b6e8-0b8271ca218a",
   "metadata": {},
   "source": [
    "<p style=\"background-color:#fff6e4; padding:15px; border-width:3px; border-color:#f5ecda; border-style:solid; border-radius:6px\"> ⏳ <b>Note:</b> If the output of the previous cell is <code>Error creating vector search index: Duplicate Index</code> you may proceed to the next cell if you intend to still use a previously created index.</p>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "654e68a1-8321-4636-8423-1835ab22b376",
   "metadata": {
    "height": 370
   },
   "outputs": [],
   "source": [
    "import openai\n",
    "\n",
    "openai.api_key = OPENAI_API_KEY\n",
    "\n",
    "def get_embedding(text):\n",
    "    \"\"\"Generate an embedding for the given text using OpenAI's API.\"\"\"\n",
    "\n",
    "    # Check for valid input\n",
    "    if not text or not isinstance(text, str):\n",
    "        return None\n",
    "\n",
    "    try:\n",
    "        # Call OpenAI API to get the embedding\n",
    "        embedding = openai.embeddings.create(\n",
    "            input=text,\n",
    "            model=\"text-embedding-3-small\", dimensions=1536).data[0].embedding\n",
    "        return embedding\n",
    "    except Exception as e:\n",
    "        print(f\"Error in get_embedding: {e}\")\n",
    "        return None"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "246983db-02d2-41a2-a9ae-48ad2f23a4ab",
   "metadata": {},
   "source": [
    "## 1.6 Compose Vector Search Query"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "884ddc3a-5cc0-47f7-b4cf-c78d7b3b0231",
   "metadata": {
    "height": 931
   },
   "outputs": [],
   "source": [
    "def vector_search(user_query, db, collection, vector_index=\"vector_index_text\"):\n",
    "    \"\"\"\n",
    "    Perform a vector search in the MongoDB collection based on the user query.\n",
    "\n",
    "    Args:\n",
    "    user_query (str): The user's query string.\n",
    "    db (MongoClient.database): The database object.\n",
    "    collection (MongoCollection): The MongoDB collection to search.\n",
    "    additional_stages (list): Additional aggregation stages to include in the pipeline.\n",
    "\n",
    "    Returns:\n",
    "    list: A list of matching documents.\n",
    "    \"\"\"\n",
    "\n",
    "    # Generate embedding for the user query\n",
    "    query_embedding = get_embedding(user_query)\n",
    "\n",
    "    if query_embedding is None:\n",
    "        return \"Invalid query or embedding generation failed.\"\n",
    "\n",
    "    # Define the vector search stage\n",
    "    vector_search_stage = {\n",
    "        \"$vectorSearch\": {\n",
    "            \"index\": vector_index, # specifies the index to use for the search\n",
    "            \"queryVector\": query_embedding, # the vector representing the query\n",
    "            \"path\": text_embedding_field_name, # field in the documents containing the vectors to search against\n",
    "            \"numCandidates\": 150, # number of candidate matches to consider\n",
    "            \"limit\": 20 # return top 20 matches\n",
    "        }\n",
    "    }\n",
    "\n",
    "    # Define the aggregate pipeline with the vector search stage and additional stages\n",
    "    pipeline = [vector_search_stage]\n",
    "\n",
    "    # Execute the search\n",
    "    results = collection.aggregate(pipeline)\n",
    "\n",
    "    explain_query_execution = db.command( # sends a database command directly to the MongoDB server\n",
    "        'explain', { # return information about how MongoDB executes a query or command without actually running it\n",
    "            'aggregate': collection.name, # specifies the name of the collection on which the aggregation is performed\n",
    "            'pipeline': pipeline, # the aggregation pipeline to analyze\n",
    "            'cursor': {} # indicates that default cursor behavior should be used\n",
    "        }, \n",
    "        verbosity='executionStats') # detailed statistics about the execution of each stage of the aggregation pipeline\n",
    "\n",
    "\n",
    "    vector_search_explain = explain_query_execution['stages'][0]['$vectorSearch']\n",
    "    millis_elapsed = vector_search_explain['explain']['collectStats']['millisElapsed']\n",
    "\n",
    "    print(f\"Total time for the execution to complete on the database server: {millis_elapsed} milliseconds\")\n",
    "\n",
    "    return list(results)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fa2107cd-c922-492a-ad4f-54cb94b436a3",
   "metadata": {},
   "source": [
    "## 1.7 Handling User Query"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b771954d-e823-42b7-9141-5f74298b332e",
   "metadata": {
    "height": 149
   },
   "outputs": [],
   "source": [
    "class SearchResultItem(BaseModel):\n",
    "    name: str\n",
    "    accommodates: Optional[int] = None\n",
    "    address: Address\n",
    "    summary: Optional[str] = None\n",
    "    description: Optional[str] = None\n",
    "    neighborhood_overview: Optional[str] = None\n",
    "    notes: Optional[str] = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "49ff7a10-0ae4-4e7d-9e30-5a144e742441",
   "metadata": {
    "height": 778
   },
   "outputs": [],
   "source": [
    "from IPython.display import display, HTML\n",
    "\n",
    "def handle_user_query(query, db, collection):\n",
    "    # Assuming vector_search returns a list of dictionaries with keys 'title' and 'plot'\n",
    "    get_knowledge = vector_search(query, db, collection)\n",
    "\n",
    "    # Check if there are any results\n",
    "    if not get_knowledge:\n",
    "        return \"No results found.\", \"No source information available.\"\n",
    "        \n",
    "     # Convert search results into a list of SearchResultItem models\n",
    "    search_results_models = [\n",
    "        SearchResultItem(**result)\n",
    "        for result in get_knowledge\n",
    "    ]\n",
    "\n",
    "    # Convert search results into a DataFrame for better rendering in Jupyter\n",
    "    search_results_df = pd.DataFrame([item.dict() for item in search_results_models])\n",
    "\n",
    "    # Generate system response using OpenAI's completion\n",
    "    completion = openai.chat.completions.create(\n",
    "        model=\"gpt-3.5-turbo\",\n",
    "        messages=[\n",
    "            {\n",
    "                \"role\": \"system\", \n",
    "                \"content\": \"You are a airbnb listing recommendation system.\"},\n",
    "            {\n",
    "                \"role\": \"user\", \n",
    "                \"content\": f\"Answer this user query: {query} with the following context:\\n{search_results_df}\"\n",
    "            }\n",
    "        ]\n",
    "    )\n",
    "\n",
    "    system_response = completion.choices[0].message.content\n",
    "\n",
    "    # Print User Question, System Response, and Source Information\n",
    "    print(f\"- User Question:\\n{query}\\n\")\n",
    "    print(f\"- System Response:\\n{system_response}\\n\")\n",
    "\n",
    "    # Display the DataFrame as an HTML table\n",
    "    display(HTML(search_results_df.to_html()))\n",
    "\n",
    "    # Return structured response and source info as a string\n",
    "    return system_response"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "41b800f9-b005-4800-af73-ed36c552e7ad",
   "metadata": {
    "height": 132
   },
   "outputs": [],
   "source": [
    "query = \"\"\"\n",
    "I want to stay in a place that's warm and friendly, \n",
    "and not too far from resturants, can you recommend a place? \n",
    "Include a reason as to why you've chosen your selection.\n",
    "\"\"\"\n",
    "handle_user_query(query, db, collection)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2021f9a3-0b67-493c-a5e3-d8ea2a695e63",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5502d2e5-c125-4141-8dae-df413c6f050d",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95a5ea3b-ca85-494d-a7b1-abc1b0584972",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a33a1f4-459a-4c49-b39e-04fb8268ab26",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "00471809-c526-4e58-833e-29c9b449ce21",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9b089114-f92c-46b8-8b51-cb218d7a8ff9",
   "metadata": {
    "height": 30
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
